from enum import Enum, EnumMeta

from .utils import AttrDict

__all__ = ["IntChoices", "StrChoices", "IntChoice", "StrChoice"]


class InvalidLabelError(KeyError):
    def __str__(self):
        # 加这个方法的原因如下：
        # str(Exception('a')) == 'a'
        # str(KeyError('a')) == "'a'"
        return super(Exception, self).__str__()


class InvalidValueError(InvalidLabelError):
    ...


def contains_chinese(chars: str) -> bool:
    '''判断字符串是否含有中文

    >>> contains_chinese('这是中文')
    True
    >>> contains_chinese('这不全是中文aaa')
    True
    >>> contains_chinese('aaa')
    False
    >>> contains_chinese('1')
    False
    '''
    return any('\u4e00' <= c <= '\u9fa5' for c in chars)


class ChoiceMeta(EnumMeta):
    def __getattribute__(cls, name):
        value = super().__getattribute__(name)
        if isinstance(value, Enum):
            value, tag = value.value, value.name
            if isinstance(value, tuple):
                value = value[0]
            value = cls.value_type(value)
            if isinstance(value, str) and contains_chinese(value):
                value = tag
        return value

    def __iter__(self):
        value_type = self.value_type
        for tag in super().__iter__():
            v = tag.value
            is_tuple = isinstance(v, tuple)
            if not is_tuple or len(v) == 1:
                value = value_type(v[0]) if is_tuple else value_type(v)
                if value_type == str and contains_chinese(value):
                    # 等号右侧有中文的情况
                    label = value
                    value = tag.name
                else:
                    label = tag.name.upper()
                yield (value, label)
            else:
                yield (value_type(v[0]), v[1])

    @property
    def Label(cls):
        d = {}
        for tag in super().__iter__():
            v = tag.value
            if isinstance(v, tuple) and len(v) > 1:
                d[tag.name] = v[1]
            else:
                d[tag.name] = tag.name.upper()
        return AttrDict(d)

    def get_label(cls, name):
        return getattr(cls.Label, name)

    def value_label(cls, value):
        return dict(cls)[cls.value_type(value)]

    def label_value(cls, label):
        try:
            value = {v: k for k, v in cls}[label]
        except KeyError:
            msg = f'{repr(label)} not in {list(cls.Label.values())}'
            raise InvalidLabelError(msg)
        return value

    def label_name(cls, label):
        return cls.value_name(cls.label_value(label))

    def value_name(cls, value):
        values = []
        for tag in super().__iter__():
            val = getattr(cls, tag.name)
            values.append(val)
            if val == cls.value_type(value):
                return tag.name
        else:
            msg = f'{repr(cls.value_type(value))} not in {values}'
            raise InvalidValueError(msg)


class IntChoiceMeta(ChoiceMeta):
    @property
    def value_type(cls):
        return int


class StrChoiceMeta(ChoiceMeta):
    @property
    def value_type(cls):
        return str


class Choice(Enum):
    """Class for creating enumerated choices."""
    def __str__(self):
        """
        Use value when cast to str, so that Choices set as model instance
        attributes are rendered as expected in templates and similar contexts.
        """
        return str(self.value)


class IntChoice(Choice, metaclass=IntChoiceMeta):
    """Class for creating enumerated integer choices.

    Usage::
        >>> class CheckChoice(IntChoice):
        ...     init = 0, "待审核"
        ...     audit = 1, "初审"
        ...     ok = 2, "复核"
        ...
        >>> list(CheckChoice) == [(0,'待审核'), (1,'初审'), (2,'复核')]
        True
        >>> CheckChoice.init == 0 and CheckChoice.audit == 1
        True
        >>> dict(CheckChoice)[CheckChoice.init] == '待审核'
        True
        >>> CheckChoice.Label.ok == '复核' == CheckChoice.get_label('ok')
        True
        >>> CheckChoice.value_label(2) == '复核' == CheckChoice.value_label(CheckChoice.ok)  # NOQA
        True
        >>> CheckChoice.label_name("待审核") == 'init'
        True
        >>> CheckChoice.value_name(0) == 'init'
        True
        >>> CheckChoice.label_name('进行') == 'init'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidLabelError: '进行' not in ['待审核', '初审', '复核']
        >>> CheckChoice.value_name(11) == 'active'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidValueError: 11 not in [0, 1, 2]

    Example:
        class Notice(models.Model):
            check = models.PositiveSmallIntegerField(choices=CheckChoice)
    """


class StrChoice(Choice, metaclass=StrChoiceMeta):
    """Class for creating enumerated string choices.

    Usage::
        >>> class StatusChoice(StrChoice):
        ...     init = 0, "初始"
        ...     active = 1, "进行中"
        ...     complete = '2', "已完成"
        ...
        >>> list(StatusChoice) == [('0','初始'),('1','进行中'),('2','已完成')]
        True
        >>> StatusChoice.init == '0' and StatusChoice.active == '1'
        True
        >>> dict(StatusChoice)[StatusChoice.init] == '初始'
        True
        >>> StatusChoice.Label.init =='初始'== StatusChoice.get_label('init')
        True
        >>> StatusChoice.value_label(0) == '初始' == StatusChoice.value_label(StatusChoice.init)  # NOQA
        True
        >>> StatusChoice.label_value('初始') == '0'
        True
        >>> StatusChoice.label_name('进行中') == 'active'
        True
        >>> StatusChoice.value_name("1") == 'active'
        True
        >>> StatusChoice.label_name('进行') == 'active'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidLabelError: '进行' not in ['初始', '进行中', '已完成']
        >>> StatusChoice.value_name(11) == 'active'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidValueError: '11' not in ['0', '1', '2']
        >>> StatusChoice.value_name(2) == 'complete'
        True
        >>> try:
        ...     StatusChoice.label_value('不存在的label')
        ... except KeyError as e:
        ...     print(e)
        ...
        '不存在的label' not in ['初始', '进行中', '已完成']
        >>> class NewChoice(StrChoice):  # 2020-03-23 最新用法(兼容旧代码)
        ...     init = "初始"
        ...     active = "进行中"
        ...     complete = "已完成"
        ...
        >>> list(NewChoice)
        [('init', '初始'), ('active', '进行中'), ('complete', '已完成')]
        >>> NewChoice.init == 'init'
        True
        >>> NewChoice.value_name('init') == 'init'
        True
        >>> NewChoice.label_name('初始') == 'init'
        True
        >>> NewChoice.value_name('initt') == 'init'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidValueError: 'initt' not in ['init', 'active', 'complete']
        >>> NewChoice.label_name('初始化') == 'init'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidLabelError: '初始化' not in ['INIT', 'ACTIVE', 'COMPLETE']
        >>> class SlimChoice(StrChoice):  # 2020/07/13 右侧非中文的情况
        ...     init = 1
        ...     active = 2
        ...     complete = 3
        ...
        >>> list(SlimChoice)
        [('1', 'INIT'), ('2', 'ACTIVE'), ('3', 'COMPLETE')]
        >>> SlimChoice.init == '1'
        True
        >>> SlimChoice.value_name(1) == 'init'
        True
        >>> SlimChoice.label_name('INIT') == 'init'
        True
        >>> SlimChoice.value_name(4) == 'complete'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidValueError: '4' not in ['1', '2', '3']
        >>> SlimChoice.label_name('COMPLETED') == 'complete'
        Traceback (most recent call last):
            ...
        toolbox.enums.InvalidLabelError: 'COMPLETED' not in ['INIT', 'ACTIVE', 'COMPLETE']
        

    Example:
        class Bill(models.Model):
            status = models.CharField(max_length=20, choices=NewChoice)
    """

IntChoices, StrChoices = IntChoice, StrChoice   # Compitable with old version
